<template>
    <div class="el-tree" :class="{
      'el-tree--highlight-current': highlightCurrent,
      'is-dragging': !!dragState.draggingNode,
      'is-drop-not-allow': !dragState.allowDrop,
      'is-drop-inner': dragState.dropType === 'inner',
    }" :style="{ height: height + '%' }" role="tree">
        <virtual-list :style="{ height: height + '%','overflow-y': 'auto' }" :data-key="getNodeKey" :data-sources="visibleList" :data-component="itemComponent" :keeps="showNumber" :extra-props="{
        renderAfterExpand,
        showCheckbox,
        renderContent,
        onNodeExpand: handleNodeExpand,
        showCount,
        showStatus,
        showOperate,
        currentSelect
      }" />
        <!-- <el-tree-node v-else v-for="child in root.childNodes" :node="child" :props="props"
            :render-after-expand="renderAfterExpand" :show-checkbox="showCheckbox" :key="getNodeKey(child)"
            :render-content="renderContent" @node-expand="handleNodeExpand">
        </el-tree-node> -->
        <div class="el-tree__empty-block" v-if="isEmpty">
            <span class="el-tree__empty-text">{{ emptyText }}</span>
        </div>
        <!-- <div v-show="dragState.showDropIndicator" class="el-tree__drop-indicator" ref="dropIndicator">
        </div> -->
    </div>
</template>

<script>
import TreeStore from './model/tree-store'
import VirtualList from 'vue-virtual-scroll-list'
import { getNodeKey, findNearestComponent } from './model/util'
import ElTreeNode from './tree-node.vue'
import ElVirtualNode from './tree-virtual-node.vue'
import { t } from 'element-ui/src/locale'
import emitter from 'element-ui/src/mixins/emitter'
import { addClass, removeClass } from 'element-ui/src/utils/dom'

export default {
    name: 'ElTree',

    mixins: [emitter],

    components: {
        VirtualList,
        ElTreeNode
    },

    data () {
        return {
            store: null,
            root: null,
            currentNode: null,
            treeItems: null,
            checkboxItems: [],
            dragState: {
                showDropIndicator: false,
                draggingNode: null,
                dropNode: null,
                allowDrop: true
            },
            itemComponent: ElVirtualNode,
            expandKeys: [],
            currentSelect: -1
        }
    },

    props: {
        data: {
            type: Array
        },
        emptyText: {
            type: String,
            default () {
                return t('el.tree.emptyText')
            }
        },
        renderAfterExpand: {
            type: Boolean,
            default: true
        },
        nodeKey: String,
        checkStrictly: Boolean,
        defaultExpandAll: Boolean,
        expandOnClickNode: {
            type: Boolean,
            default: true
        },
        checkOnClickNode: Boolean,
        checkDescendants: {
            type: Boolean,
            default: false
        },
        autoExpandParent: {
            type: Boolean,
            default: true
        },
        defaultCheckedKeys: Array,
        defaultExpandedKeys: Array,
        currentNodeKey: [String, Number],
        renderContent: Function,
        showCheckbox: {
            type: Boolean,
            default: false
        },
        showCount: {             // 是否展示分组数量
            type: Boolean,
            default: false
        },
        showNumber: {           // 页面最多展示节点数
            type: Number,
            default: 20
        },
        showStatus: {          // 是否展示设备状态
            type: Boolean,
            default: false
        },
        draggable: {
            type: Boolean,
            default: false
        },
        showOperate: {       // 是否展示操作按钮
            type: Boolean,
            default: false
        },
        allowDrag: Function,
        allowDrop: Function,
        props: {
            default () {
                return {
                    children: 'children',
                    label: 'title',
                    disabled: 'disabled'
                }
            }
        },
        lazy: {
            type: Boolean,
            default: false
        },
        highlightCurrent: Boolean,
        load: Function,
        filterNodeMethod: Function,
        accordion: Boolean,
        indent: {
            type: Number,
            default: 18
        },
        iconClass: String,
        height: {
            type: Number,
            default: 0
        },
        extraLine: {
            type: Number,
            default: 8
        }
    },

    computed: {
        children: {
            set (value) {
                this.data = value
            },
            get () {
                return this.data
            }
        },

        treeItemArray () {
            return Array.prototype.slice.call(this.treeItems)
        },

        isEmpty () {
            const { childNodes } = this.root
            return (
                !childNodes ||
                childNodes.length === 0 ||
                childNodes.every(({ visible }) => !visible)
            )
        },

        visibleList () {
            return this.flattenTree(this.root.childNodes)
        }
    },

    watch: {
        defaultCheckedKeys (newVal) {
            this.store.setDefaultCheckedKey(newVal)
        },

        expandKeys (newVal) {
            this.store.defaultExpandedKeys = newVal
            this.store.setDefaultExpandedKeys(newVal)
        },

        data (newVal) {
            this.store.setData(newVal)
        },

        checkboxItems (val) {
            Array.prototype.forEach.call(val, (checkbox) => {
                checkbox.setAttribute('tabindex', -1)
            })
        },

        checkStrictly (newVal) {
            this.store.checkStrictly = newVal
        }
    },

    methods: {
        flattenTree (datas) {
            return datas.reduce((conn, data) => {
                conn.push(data)
                if (data.expanded && data.childNodes.length) {
                    conn.push(...this.flattenTree(data.childNodes))
                }

                return conn
            }, [])
        },
        filter (value) {
            if (!this.filterNodeMethod) { throw new Error('[Tree] filterNodeMethod is required when filter') }
            this.store.filter(value)
        },

        getNodeKey (node) {
            return getNodeKey(this.nodeKey, node.data)
        },

        getNodePath (data) {
            if (!this.nodeKey) { throw new Error('[Tree] nodeKey is required in getNodePath') }
            const node = this.store.getNode(data)
            if (!node) return []
            const path = [node.data]
            let parent = node.parent
            while (parent && parent !== this.root) {
                path.push(parent.data)
                parent = parent.parent
            }
            return path.reverse()
        },

        getCheckedNodes (leafOnly, includeHalfChecked) {
            return this.store.getCheckedNodes(leafOnly, includeHalfChecked)
        },

        getCheckedKeys (leafOnly) {
            return this.store.getCheckedKeys(leafOnly)
        },

        getCurrentNode () {
            const currentNode = this.store.getCurrentNode()
            return currentNode ? currentNode.data : null
        },

        getCurrentKey () {
            if (!this.nodeKey) { throw new Error('[Tree] nodeKey is required in getCurrentKey') }
            const currentNode = this.getCurrentNode()
            return currentNode ? currentNode[this.nodeKey] : null
        },

        setCheckedNodes (nodes, leafOnly) {
            if (!this.nodeKey) { throw new Error('[Tree] nodeKey is required in setCheckedNodes') }
            this.store.setCheckedNodes(nodes, leafOnly)
        },

        setCheckedKeys (keys, leafOnly) {
            if (!this.nodeKey) { throw new Error('[Tree] nodeKey is required in setCheckedKeys') }
            this.store.setCheckedKeys(keys, leafOnly)
        },

        setChecked (data, checked, deep) {
            this.store.setChecked(data, checked, deep)
        },

        getHalfCheckedNodes () {
            return this.store.getHalfCheckedNodes()
        },

        getHalfCheckedKeys () {
            return this.store.getHalfCheckedKeys()
        },

        setCurrentNode (node) {
            if (!this.nodeKey) { throw new Error('[Tree] nodeKey is required in setCurrentNode') }
            this.store.setUserCurrentNode(node)
        },

        setCurrentKey (key) {
            if (!this.nodeKey) { throw new Error('[Tree] nodeKey is required in setCurrentKey') }
            this.store.setCurrentNodeKey(key)
        },

        getNode (data) {
            return this.store.getNode(data)
        },

        remove (data) {
            this.store.remove(data)
        },

        append (data, parentNode) {
            this.store.append(data, parentNode)
        },

        insertBefore (data, refNode) {
            this.store.insertBefore(data, refNode)
        },

        insertAfter (data, refNode) {
            this.store.insertAfter(data, refNode)
        },

        handleNodeExpand (nodeData, node, instance) {
            this.broadcast('ElTreeNode', 'tree-node-expand', node)
            this.$emit('node-expand', nodeData, node, instance)
        },

        updateKeyChildren (key, data) {
            if (!this.nodeKey) { throw new Error('[Tree] nodeKey is required in updateKeyChild') }
            this.store.updateChildren(key, data)
        },

        initTabIndex () {
            this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]')
            this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]')
            const checkedItem = this.$el.querySelectorAll(
                '.is-checked[role=treeitem]'
            )
            if (checkedItem.length) {
                checkedItem[0].setAttribute('tabindex', 0)
                return
            }
            this.treeItems[0] && this.treeItems[0].setAttribute('tabindex', 0)
        },

        handleKeydown (ev) {
            const currentItem = ev.target
            if (currentItem.className.indexOf('el-tree-node') === -1) return
            const keyCode = ev.keyCode
            this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]')
            const currentIndex = this.treeItemArray.indexOf(currentItem)
            let nextIndex
            if ([38, 40].indexOf(keyCode) > -1) {

                // up、down
                ev.preventDefault()
                if (keyCode === 38) {

                    // up
                    nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0
                } else {
                    nextIndex =
                        currentIndex < this.treeItemArray.length - 1 ? currentIndex + 1 : 0
                }
                this.treeItemArray[nextIndex].focus() // 选中
            }
            if ([37, 39].indexOf(keyCode) > -1) {

                // left、right 展开
                ev.preventDefault()
                currentItem.click() // 选中
            }
            const hasInput = currentItem.querySelector('[type="checkbox"]')
            if ([13, 32].indexOf(keyCode) > -1 && hasInput) {

                // space enter选中checkbox
                ev.preventDefault()
                hasInput.click()
            }
        }
    },

    created () {
        this.isTree = true

        this.store = new TreeStore({
            key: this.nodeKey,
            data: this.data,
            lazy: this.lazy,
            props: this.props,
            load: this.load,
            currentNodeKey: this.currentNodeKey,
            checkStrictly: this.checkStrictly,
            checkDescendants: this.checkDescendants,
            defaultCheckedKeys: this.defaultCheckedKeys,
            defaultExpandedKeys: this.expandKeys,
            autoExpandParent: this.autoExpandParent,
            defaultExpandAll: this.defaultExpandAll,
            filterNodeMethod: this.filterNodeMethod
        })

        this.$on('saveOpenStatus', (node) => {
            if (node.data.isGroup) {
                if (node.expanded) {
                    this.expandKeys.push(node.data.key)
                } else {
                    let index = this.expandKeys.indexOf(node.data.key)
                    if (index != -1) {
                        this.expandKeys.splice(index, 1)
                    }
                }
            } else {
                this.currentSelect = node.data.key
            }

        })

        this.root = this.store.root

        let dragState = this.dragState
        this.$on('tree-node-drag-start', (event, treeNode) => {
            if (
                typeof this.allowDrag === 'function' &&
                !this.allowDrag(treeNode.node)
            ) {
                event.preventDefault()
                return false
            }
            event.dataTransfer.effectAllowed = 'move'

            // wrap in try catch to address IE's error when first param is 'text/plain'
            try {

                // setData is required for draggable to work in FireFox
                // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
                event.dataTransfer.setData('text/plain', '')
            } catch (e) { }
            dragState.draggingNode = treeNode
            this.$emit('node-drag-start', treeNode.node, event)
        })

        this.$on('tree-node-drag-over', (event, treeNode) => {
            const dropNode = findNearestComponent(event.target, 'ElTreeNode')
            const oldDropNode = dragState.dropNode
            if (oldDropNode && oldDropNode !== dropNode) {
                removeClass(oldDropNode.$el, 'is-drop-inner')
            }
            const draggingNode = dragState.draggingNode
            if (!draggingNode || !dropNode) return

            let dropPrev = true
            let dropInner = true
            let dropNext = true
            let userAllowDropInner = true
            if (typeof this.allowDrop === 'function') {
                dropPrev = this.allowDrop(draggingNode.node, dropNode.node, 'prev')
                userAllowDropInner = dropInner = this.allowDrop(
                    draggingNode.node,
                    dropNode.node,
                    'inner'
                )
                dropNext = this.allowDrop(draggingNode.node, dropNode.node, 'next')
            }
            event.dataTransfer.dropEffect = dropInner ? 'move' : 'none'
            if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) {
                if (oldDropNode) {
                    this.$emit(
                        'node-drag-leave',
                        draggingNode.node,
                        oldDropNode.node,
                        event
                    )
                }
                this.$emit('node-drag-enter', draggingNode.node, dropNode.node, event)
            }

            if (dropPrev || dropInner || dropNext) {
                dragState.dropNode = dropNode
            }

            if (dropNode.node.nextSibling === draggingNode.node) {
                dropNext = false
            }
            if (dropNode.node.previousSibling === draggingNode.node) {
                dropPrev = false
            }
            if (dropNode.node.contains(draggingNode.node, false)) {
                dropInner = false
            }
            if (
                draggingNode.node === dropNode.node ||
                draggingNode.node.contains(dropNode.node)
            ) {
                dropPrev = false
                dropInner = false
                dropNext = false
            }

            const targetPosition = dropNode.$el.getBoundingClientRect()
            const treePosition = this.$el.getBoundingClientRect()

            let dropType
            const prevPercent = dropPrev
                ? dropInner
                    ? 0.25
                    : dropNext
                        ? 0.45
                        : 1
                : -1
            const nextPercent = dropNext
                ? dropInner
                    ? 0.75
                    : dropPrev
                        ? 0.55
                        : 0
                : 1

            let indicatorTop = -9999
            const distance = event.clientY - targetPosition.top
            if (distance < targetPosition.height * prevPercent) {
                dropType = 'before'
            } else if (distance > targetPosition.height * nextPercent) {
                dropType = 'after'
            } else if (dropInner) {
                dropType = 'inner'
            } else {
                dropType = 'none'
            }

            const iconPosition = dropNode.$el
                .querySelector('.el-tree-node__expand-icon')
                .getBoundingClientRect()
            const dropIndicator = this.$refs.dropIndicator
            if (dropType === 'before') {
                indicatorTop = iconPosition.top - treePosition.top
            } else if (dropType === 'after') {
                indicatorTop = iconPosition.bottom - treePosition.top
            }
            dropIndicator.style.top = indicatorTop + 'px'
            dropIndicator.style.left = iconPosition.right - treePosition.left + 'px'

            if (dropType === 'inner') {
                addClass(dropNode.$el, 'is-drop-inner')
            } else {
                removeClass(dropNode.$el, 'is-drop-inner')
            }

            dragState.showDropIndicator =
                dropType === 'before' || dropType === 'after'
            dragState.allowDrop = dragState.showDropIndicator || userAllowDropInner
            dragState.dropType = dropType
            this.$emit('node-drag-over', draggingNode.node, dropNode.node, event)
        })

        this.$on('tree-node-drag-end', (event) => {
            const { draggingNode, dropType, dropNode } = dragState
            event.preventDefault()
            event.dataTransfer.dropEffect = 'move'

            if (draggingNode && dropNode) {
                const draggingNodeCopy = { data: draggingNode.node.data }
                if (dropType !== 'none') {
                    draggingNode.node.remove()
                }
                if (dropType === 'before') {
                    dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node)
                } else if (dropType === 'after') {
                    dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node)
                } else if (dropType === 'inner') {
                    dropNode.node.insertChild(draggingNodeCopy)
                }
                if (dropType !== 'none') {
                    this.store.registerNode(draggingNodeCopy)
                }

                removeClass(dropNode.$el, 'is-drop-inner')

                this.$emit(
                    'node-drag-end',
                    draggingNode.node,
                    dropNode.node,
                    dropType,
                    event
                )
                if (dropType !== 'none') {
                    this.$emit(
                        'node-drop',
                        draggingNode.node,
                        dropNode.node,
                        dropType,
                        event
                    )
                }
            }
            if (draggingNode && !dropNode) {
                this.$emit('node-drag-end', draggingNode.node, null, dropType, event)
            }

            dragState.showDropIndicator = false
            dragState.draggingNode = null
            dragState.dropNode = null
            dragState.allowDrop = true
        })
    },

    mounted () {
        this.initTabIndex()
        this.$el.addEventListener('keydown', this.handleKeydown)
    },

    updated () {
        this.treeItems = this.$el.querySelectorAll('[role=treeitem]')
        this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]')
    }
}
</script>

<style lang="scss" src="./tree.scss"></style>